﻿//-------------------------------------------------------------------------------
// <copyright file="LinqToEntitiesWorkspace.cs" company="bbv Software Services AG">
//   Copyright (c) 2008 bbv Software Services AG
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.
// </copyright>
//------------------------------------------------------------------------------

using System.Linq.Expressions;

namespace bbv.DomainDrivenDesign.Workspaces
{
    using System;
    using System.Data;
    using System.Data.Objects;
    using System.Data.Objects.DataClasses;
    using System.Reflection;
    using Contexts;
    using Interfaces;
    using log4net;
    using QueryableContexts;

    /// <summary>
    /// Implements a unit of work for the ADO .Net object/relational mapping framework 
    /// a.k.a. EntityFramework.
    /// </summary>
    public class LinqToEntitiesWorkspace : IWorkspace
    {
        #region logger

        /// <summary>
        /// Type specific logger 
        /// </summary>
        private static readonly ILog Log =
            LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        #endregion

        /// <summary>
        /// Holds the <see cref="ObjectContext"/> of the Entity framework.
        /// </summary>
        private readonly ObjectContext context;

        /// <summary>
        /// Initializes a new instance of the <see cref="LinqToEntitiesWorkspace"/> class.
        /// </summary>
        /// <param name="objectContext">The object context.</param>
        public LinqToEntitiesWorkspace(ObjectContext objectContext)
        {
            if (objectContext == null)
            {
                throw new ArgumentNullException("objectContext");
            }

            this.context = objectContext;
            this.context.ObjectStateManager.ObjectStateManagerChanged += this.OnObjectStateManagerChanged;
        }

        /// <summary>
        /// Gets the underlying connection of the unit of work.
        /// </summary>
        /// <value>The connection as a <see cref="IDbConnection"/>.</value>
        public IDbConnection Connection
        {
            get { return this.context.Connection; }
        }

        /// <summary>
        /// Persists all pending updates to the data source.
        /// </summary>
        /// <exception cref="OptimisticOfflineLockException"/>"
        public void SubmitChanges()
        {
            try
            {
                this.context.SaveChanges();
            }
            catch (OptimisticConcurrencyException e)
            {
                Log.Debug("Optimistic concurrency error.");
                Log.Debug(e.Message);

                throw new OptimisticOfflineLockException(e.Message, e);
            }
        }

        /// <summary>
        /// Accepts all changes made to objects 
        /// in the <see cref="IWorkspace"/> context.
        /// </summary>
        public void AcceptAllChanges()
        {
            this.context.AcceptAllChanges();
        }

        public T GetById<T, K>(K id) where T : class
        {
            throw new NotImplementedException();
        }


        /// <summary>
        /// Return the persistent instance of the given named entity with the given identifier,
        /// or null if there is no such persistent instance. (If the instance, or a proxy for the
        /// instance, is already associated with the session, return that instance or proxy)
        /// </summary>
        /// <typeparam name="T">the type of the entity.</typeparam>
        /// <param name="entityWithKey">an entity with a key.</param>
        /// <returns>a persistent instance or null.</returns>
        /// <exception cref="ArgumentException">if the argument does not implement the interface <see cref="IEntityWithKey"></see>.</exception>
        public T Get<T>(T entityWithKey) where T : class
        {
            if (!(entityWithKey is IEntityWithKey))
            {
                throw new ArgumentException("The argument does not implement the interface IEntityWithKey", "entityWithKey");
            }

            var key = this.context.CreateEntityKey(this.context.GetEntitySetName<T>(), entityWithKey);

            // Get the original item based on the entity key from the context
            // or from the database.
            return this.context.GetObjectByKey(key) as T;
        }

        /// <summary>
        /// Save an entity in a pending insert state to the <see cref="IWorkspace"/> context.
        /// </summary>
        /// <typeparam name="T">The type of the entity.</typeparam>
        /// <param name="entity">A transient instance of a persistent class.</param>
        public void Add<T>(T entity) where T : class
        {
            if (entity.GetType().IsAssignableFrom(typeof(EntityObject)))
            {
                throw new ArgumentException("The argument does not inherit from EntityObject", "entity");
            }

            this.context.AddObject(this.context.GetEntitySetName<T>(), entity);
        }

        public T Create<T>(T entity) where T : class
        {
            Add(entity);
            this.context.AddObject(this.context.GetEntitySetName<T>(), entity);
            SubmitChanges();
            return entity;
        }

        /// <summary>
        /// Remove an entity from this table into a pending delete state.
        /// </summary>
        /// <typeparam name="T">The type of the entity.</typeparam>
        /// <param name="entity">The instance to be removed.</param>
        public void Delete<T>(T entity) where T : class
        {
            if (entity.GetType().IsAssignableFrom(typeof(EntityObject)))
            {
                throw new ArgumentException("The argument does not inherit from EntityObject", "entity");
            }

            this.context.DeleteObject(entity);
        }

        /// <summary>
        /// Update the persistent instance with the identifier of the given transient instance.
        /// </summary>
        /// <typeparam name="T">The type of the entity.</typeparam>
        /// <param name="entity">The detached object that has property updates
        /// to apply to the original object.</param>
        /// <remarks>
        /// If there is a persistent instance with the same identifier, an exception is thrown. If
        /// the given transient instance has a <see langword="null"/> identifier, an exception will be thrown.
        /// </remarks>
        public void Update<T>(T entity) where T : class
        {
            if (entity.GetType().IsAssignableFrom(typeof(EntityObject)))
            {
                throw new ArgumentException("The argument does not inherit from EntityObject", "entity");
            }

            var key = this.context.CreateEntityKey(this.context.GetEntitySetName<T>(), entity);
            object originalItem;

            // Get the original item based on the entity key from the context
            // or from the database.
            if (this.context.TryGetObjectByKey(key, out originalItem))
            {
                // Call the ApplyPropertyChanges method to apply changes
                // from the updated item to the original version.
                this.context.ApplyPropertyChanges(
                    key.EntitySetName, entity);
            }
            else
            {
                Log.Error("Original object cold not been found on ObjectContext and database.");
                throw new ObjectNotFoundException("Original object cold not been found on ObjectContext and database.");
            }
        }

        /// <summary>
        /// Cleans all cached objects within the <see cref="IWorkspace"/>.
        /// </summary>
        public void Clean()
        {
            this.context.ClearCache();
        }

        /// <summary>
        /// Updates an object in the object context with data from the persisted store.
        /// Overrides all the current values with the values from the database.
        /// </summary>
        /// <typeparam name="T">The type of the entity.</typeparam>
        /// <param name="entity">The entity to be refreshed.</param>
        public void Refresh<T>(T entity) where T : class
        {
            this.context.Refresh(RefreshMode.StoreWins, entity);
        }

        /// <summary>
        /// Attach an object or object graph to the object context if the object
        /// has an entity key.
        /// </summary>
        /// <typeparam name="T">The type of the entity.</typeparam>
        /// <param name="entityWithKey">The entity.</param>
        public void Attach<T>(T entityWithKey) where T : class
        {
            if (!(entityWithKey is IEntityWithKey))
            {
                throw new ArgumentException("The argument does not implement the interface IEntityWithKey", "entityWithKey");
            }

            this.context.Attach(entityWithKey as IEntityWithKey);
        }

        /// <summary>
        /// Removes the specified object from the <see cref="IWorkspace"/> context.
        /// </summary>
        /// <typeparam name="T">The type of the entity.</typeparam>
        /// <param name="entity">The entity.</param>
        public void Detach<T>(T entity) where T : class
        {
            if (entity.GetType().IsAssignableFrom(typeof(EntityObject)))
            {
                throw new ArgumentException("The argument does not inherit from EntityObject", "entity");
            }

            this.context.Detach(entity);
        }

        /// <summary>
        /// Creates an <see cref="T:bbv.DomainDrivenDesign.Interfaces.IQueryableContext`1"/> in the current object context
        /// </summary>
        /// <typeparam name="T">The type of the IQueryableContext.</typeparam>
        /// <returns>An <see cref="T:bbv.DomainDrivenDesign.Interfaces.IQueryableContext`1"/> of the specified type.</returns>
        public IQueryableContext<T> CreateQuery<T>() where T : class
        {
            return new LinqToEntitiesQueryableContext<T>(this.context);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, 
        /// releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.context.Dispose();
        }

        /// <summary>
        /// Handles the ObjectStateManagerChanged event of the ObjectStateManager control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.ComponentModel.CollectionChangeEventArgs"/> instance containing the event data.</param>
        private void OnObjectStateManagerChanged(object sender, System.ComponentModel.CollectionChangeEventArgs e)
        {
            if (e.Action == System.ComponentModel.CollectionChangeAction.Add)
            {
                var entity = e.Element as EntityObject;
                if (entity != null && entity.EntityState == EntityState.Added)
                {
                    this.GenerateId(entity);
                }
            }
        }
    }
}
